package org.hashdb.client.console;

import jline.Terminal;
import jline.TerminalFactory;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
import lombok.Getter;
import lombok.Setter;
import org.hashdb.client.config.AppConfig;
import org.hashdb.client.console.completer.*;
import org.hashdb.client.console.decorator.StringDecorator;
import org.hashdb.client.net.bio.MessageChannel;
import org.hashdb.client.net.nio.Protocol;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * Date: 2024/1/8 1:27
 *
 * @author huanyuMake-pecdle
 * @version 0.0.1
 */
public class AppConsoleReader extends ConsoleReader {

    private final CompleterManager completerManager = new CompleterManager();

    private State state = State.NORMAL;

    public State state() {
        return state;
    }

    public enum State {
        WAIT_FOR_INPUT,
        NORMAL
    }

    private String lastPrompt = "$ >";

    @Getter
    @Setter
    private String messagePrefix = StringDecorator.green("[MESSAGE] ");

    @Getter
    @Setter
    private String errorPrefix = StringDecorator.red("[ERROR] ");

    @Setter
    @Getter
    private boolean reechoInput = false;

    private final Charset outputEncoding;

    private final List<Runnable> blockedTask = new Vector<>();

    public AppConsoleReader(String appName, InputStream in, OutputStream out, Terminal term, Charset encoding) throws IOException {
        super(appName, in, out, term, encoding.name());
        outputEncoding = encoding;
    }

    {
        completerManager.onClient();
        setHandleUserInterrupt(true);
    }

    protected static AppConsoleReader newConsoleReader() throws IOException {
        return newConsoleReader(StandardCharsets.UTF_8);
    }

    public static AppConsoleReader newConsoleReader(Charset outputEncoding) throws IOException {
        Terminal terminal = TerminalFactory.create();
        return new AppConsoleReader(AppConfig.getInstance().getAppName(), System.in, System.out, terminal, outputEncoding);
    }

    public boolean ok(String prompt, String msg) {
        return ok(prompt, msg, false);
    }

    public synchronized boolean ok(String prompt, String msg, boolean defaultChoice) {
        println(msg + " (y/n default: " + (defaultChoice ? 'y' : 'n') + ")");
        boolean isPermitCompleteClient = completerManager.isClientCmdAdded();
        boolean isPermitCompleteRemote = completerManager.isRemoteAdded();
        if (isPermitCompleteClient) {
            completerManager.offClient();
        }
        if (isPermitCompleteRemote) {
            completerManager.offRemote();
        }
        completerManager.onOk();
        Runnable rollback = () -> {
            if (isPermitCompleteRemote) {
                completerManager.onRemote();
            }
            if (isPermitCompleteClient) {
                completerManager.onClient();
            }
            completerManager.offOk();
        };
        try {
            String ok;
            try {
                ok = readLine(prompt);
                if (ok.isEmpty()) {
                    throw new UserInterruptException(ok);
                }
            } catch (UserInterruptException e) {
                rollback.run();
                return defaultChoice;
            }
            rollback.run();
            return "y".equalsIgnoreCase(ok) || "yes".equalsIgnoreCase(ok);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean ok(String msg) {
        return ok(lastPrompt, msg);
    }

    public boolean ok(String msg, boolean defaultChoice) {
        return ok(lastPrompt, msg, defaultChoice);
    }


    public String defaultLine(String prompt, String defaultInput) throws IOException {
        String res = readLine(prompt);
        return res != null && !res.isEmpty() ? res : defaultInput;
    }

    @Override
    public synchronized String readLine(String prompt, Character mask, String buffer) throws IOException {
        if (!blockedTask.isEmpty()) {
            for (Runnable task : blockedTask) {
                task.run();
            }
            blockedTask.clear();
        }
        state = State.WAIT_FOR_INPUT;
        String res;
        try {
            res = super.readLine(prompt, mask, buffer);
        } catch (Exception e) {
            state = State.NORMAL;
            throw e;
        } finally {
            state = State.NORMAL;
        }
        lastPrompt = prompt;
        String realLine = res != null && !res.isBlank() ? res.trim() : "";
        if (reechoInput) {
            println("[REECHO] '" + realLine + "'");
        }
        return realLine;
    }

    public void messageLn(String msg) {
        println(messagePrefix + msg);
    }

    public void message(String msg) {
        try {
            print(messagePrefix + msg);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void print(CharSequence s) throws IOException {
        if (state == State.WAIT_FOR_INPUT) {
            blockedTask.add(() -> {
                try {
                    print(s);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            return;
        }
        checkNewLine();
        super.print(s);
    }

    @Override
    public void println(CharSequence s) {
        if (state == State.WAIT_FOR_INPUT) {
            blockedTask.add(() -> println(s));
            return;
        }
        try {
            checkNewLine();
            super.println(s);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void errorLn(String msg) {
        println(errorPrefix + msg);
    }

    public void errorLn(String msg, Object... args) {
        println(errorPrefix + String.format(msg, args));
    }

    public void printStackTrace(Throwable t) {
        if (state == State.WAIT_FOR_INPUT) {
            blockedTask.add(() -> printStackTrace(t));
            return;
        }
        Objects.requireNonNull(t);
        checkNewLine();
        var buffer = new ByteArrayOutputStream();
        t.printStackTrace(new PrintWriter(new OutputStreamWriter(buffer)));
        errorLn(buffer.toString(outputEncoding));
    }

    protected void checkNewLine() {
        if (getCursorBuffer().length() > 0) {
            try {
                println();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void registerRemoteCompleter(MessageChannel messageChannel) {
        Objects.requireNonNull(messageChannel);
        messageChannel.onClosed(completerManager::unregisterRemote);
        messageLn("RemoteCompleter Registered");
        completerManager.registerRemote("dbnameCompleter", new DatabaseNameCompleter(messageChannel, this));
        completerManager.registerRemote("systemCommand", new IgnoreCaseStringsCompleter("dbshow", "dbcurrent", "dbuse"));
        completerManager.registerRemote("suppliers", new SupplierKeywordCompleter());
        completerManager.registerRemote("consumers", new ConsumerKeywordAggregateCompleter());
        completerManager.registerRemote("options", new OptionsCompleter());
    }

    public String readPassword(String prompt) throws IOException {
        return readLine(prompt, '*', "");
    }

    public synchronized ReadLineConfig read() {
        return new ReadLineConfig();
    }

    public synchronized ReadLineConfig read(String prompt) {
        return new ReadLineConfig().prompt(prompt);
    }

    public static BootstrapReader bootstrap() throws IOException {
        return new BootstrapReader();
    }

    public static class BootstrapReader extends AppConsoleReader {
        {
            removeCompleter(CompleterManager.clientCmdCompleter);
            SortedMap<String, Charset> charsets = Charset.availableCharsets();
            String[] charsetNames = charsets.keySet().stream().flatMap(name -> Stream.of(name, name.toLowerCase())).toArray(String[]::new);
            var charsetCompleter = new StringsCompleter(charsetNames);
            addCompleter(charsetCompleter);
        }

        public BootstrapReader() throws IOException {
            super(AppConfig.getInstance().getAppName() + " [BOOTSTRAP]", System.in, System.out, TerminalFactory.create(), StandardCharsets.UTF_8);
        }

        public Charset queryOutputCharset() throws IOException {
            getCursorBuffer().clear();
            Charset charset = StandardCharsets.UTF_8;
            try {
                messageLn("set your charset in cmd:(press 'Enter' to use UTF-8)");
                while (true) {
                    var charsetName = readLine("[CHARSET] >");
                    if (charsetName.isEmpty()) {
                        break;
                    }
                    charset = getOutputEncoding(charsetName);
                    messageLn("input a sentence encoded by this charset(press 'Enter' to skip garbled code test)");
                    String sentence = readLine("[SENTENCE] >");
                    if (sentence.isEmpty()) {
                        return charset;
                    }
                    messageLn("echo '" + new String(sentence.getBytes(), charset) + "'");
                    if (!ok("[CONFIRM] >", "Is there any garbled code in above sentence?")) {
                        return charset;
                    }
                    messageLn("may be you set a warn charset of current cmd, please set again:(press 'Enter' to use UTF-8)");
                }
            } catch (UserInterruptException e) {
                System.exit(0);
            }
            return charset;
        }


        private Charset getOutputEncoding(String charsetName) throws IOException {
            String name = charsetName;
            while (true) {
                try {
                    return Charset.forName(name);
                } catch (UnsupportedCharsetException | IllegalCharsetNameException e) {
                    errorLn("can not support charset '" + charsetName + "'. choose another(press 'Enter' to use UTF-8):");
                    name = defaultLine("[CHARSET] >", StandardCharsets.UTF_8.name());
                }
            }
        }
    }

    public CompleterManager completerManager() {
        return completerManager;
    }

    public class CompleterManager {
        private final static Completer clientCmdCompleter;
        private final static StringsCompleter okCompleter = new StringsCompleter("y", "n", "yes", "no");
        private final Map<String, Completer> remoteCompleter = new HashMap<>();

        private StringsCompleter protocolCompleter;
        @Getter
        private boolean remoteAdded = false;
        @Getter
        private boolean clientCmdAdded = false;
        @Getter
        private boolean okAdded = false;

        @Getter
        private boolean protocolAdded = false;

        /**
         * 记录
         */
        private CompleterSavepoint completerMark;

        private final CompleterSwitcher[] COMPLETER_SWITCHERS = Arrays.stream(CompleterManager.class.getDeclaredFields()).filter(field -> field.getName().toLowerCase().endsWith("added"))
                .map(CompleterSwitcher::new)
                .toArray(CompleterSwitcher[]::new);

        private class CompleterSwitcher {
            private final Field addedField;
            private final Method onMethod;
            private final Method offMethod;

            private CompleterSwitcher(Field addedField) {
                this.addedField = addedField;
                int postfix = addedField.getName().indexOf("Added");
                if (postfix == -1) {
                    throw new IllegalArgumentException("illegal added field");
                }
                String completerName = addedField.getName().substring(0, postfix);
                onMethod = Arrays.stream(CompleterManager.class.getMethods())
                        .filter(method -> method.getName().toLowerCase().equals("on" + completerName))
                        .findFirst().orElseThrow(() -> new RuntimeException("can not find 'on' method"));
                onMethod.setAccessible(true);
                offMethod = Arrays.stream(CompleterManager.class.getMethods())
                        .filter(method -> method.getName().toLowerCase().equals("off" + completerName))
                        .findFirst().orElseThrow(() -> new RuntimeException("can not find 'off' method"));
                offMethod.setAccessible(true);
            }

            public boolean value() {
                try {
                    return (boolean) addedField.get(this);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }

            public void on() {
                try {
                    onMethod.invoke(CompleterManager.this);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }

            public void off() {
                try {
                    offMethod.invoke(CompleterManager.this);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        public class CompleterSavepoint {
            private final List<CompleterSwitcher> activeCompleter;

            private boolean rollback;

            CompleterSavepoint(List<CompleterSwitcher> activeCompleter) {
                this.activeCompleter = activeCompleter;
            }

            public void rollback() {
                if (rollback) {
                    throw new IllegalStateException();
                }
                for (CompleterSwitcher switcher : activeCompleter) {
                    switcher.on();
                }
                onRollback();
                rollback = true;
                activeCompleter.clear();
            }

            protected void onRollback() {
            }
        }

        static {
            String[] words = {
                    "exit",
                    "reconnect",
                    "auth",
//                "help"
            };
            clientCmdCompleter = new IgnoreCaseStringsCompleter(words);
        }

        public CompleterSavepoint savepoint() {
            return new CompleterSavepoint(activeCompleterSwitcher());
        }

        private List<CompleterSwitcher> activeCompleterSwitcher() {
            return Arrays.stream(COMPLETER_SWITCHERS)
                    .filter(CompleterSwitcher::value)
                    .toList();
        }

        public void markActiveCompleter() {
            completerMark = new CompleterSavepoint(activeCompleterSwitcher()) {
                @Override
                protected void onRollback() {
                    completerMark = null;
                }
            };
        }

        public void undoActiveCompleter() {
            if (completerMark == null) {
                throw new IllegalStateException("no active completer mark");
            }
            completerMark.rollback();
        }

        private void registerRemote(String name, Completer completer) {
            remoteCompleter.putIfAbsent(name, completer);
        }

        public void onRemote() {
            if (remoteAdded) {
                return;
            }
            for (Completer completer : remoteCompleter.values()) {
                addCompleter(completer);
            }
            remoteAdded = true;
        }

        public void offRemote() {
            if (!remoteAdded) {
                return;
            }
            for (Completer completer : remoteCompleter.values()) {
                removeCompleter(completer);
            }
            remoteAdded = false;
        }

        public void onOk() {
            if (okAdded) {
                return;
            }
            addCompleter(okCompleter);
            okAdded = true;
        }

        public void offOk() {
            if (!okAdded) {
                return;
            }
            removeCompleter(okCompleter);
            okAdded = false;
        }

        public void onClient() {
            if (clientCmdAdded) {
                return;
            }
            addCompleter(clientCmdCompleter);
            clientCmdAdded = true;
        }

        public void offClient() {
            if (!clientCmdAdded) {
                return;
            }
            removeCompleter(clientCmdCompleter);
            clientCmdAdded = false;
        }

        public void onProtocol() {
            if (protocolAdded) {
                return;
            }
            if (protocolCompleter == null) {
                protocolCompleter = new StringsCompleter(Arrays.stream(Protocol.values()).map(Enum::name).toArray(String[]::new));
            }
            addCompleter(protocolCompleter);
            protocolAdded = true;
        }

        public void offProtocol() {
            if (!protocolAdded) {
                return;
            }
            removeCompleter(protocolCompleter);
            protocolAdded = false;
        }

        private Completer unregisterRemote(String name) {
            Completer completer = remoteCompleter.remove(name);
            if (completer == null) {
                return null;
            }
            removeCompleter(completer);
            remoteAdded = false;
            clientCmdAdded = false;
            okAdded = false;
            return completer;
        }

        private void unregisterRemote() {
            for (Completer completer : remoteCompleter.values()) {
                removeCompleter(completer);
            }
            remoteCompleter.clear();
            remoteAdded = false;
        }


    }

    public class ReadLineConfig {
        private boolean notEmpty = false;

        private boolean denyInterrupt = false;
        /**
         * 如果不为null, 且当 {@link AppConsoleReader} 设置了
         * {@link #setHandleUserInterrupt(boolean true)}
         * 则 {@link #readLine()} 抛出 {@link UserInterruptException} 时
         * 默认catch该异常, 以该code退出当前命令行程序.
         */
        private Integer exitCode;

        /**
         * 掩码, 比如在输入密码时,可以设置为'*', 以该字符串替换真实的每个字符
         */
        private Character mask;

        private String prompt;

        private Function<String, String> newPrompt;

        private Function<UserInterruptException, String> interruptNewPrompt;

        private final List<String> buffer = new LinkedList<>();

        /**
         * 获取输入后, 调用其检查输入值,如果通过, 则返回该值, 否则, 让用户继续输入
         */
        private Predicate<String> checker;

        private List<Completer> tempCompleter;

        private boolean tempOffActiveCompleter;

        public String apply() throws IOException {
            String inputBuffer = String.join("", buffer);
            String currentPrompt = prompt;
            if (tempCompleter != null) {
                tempCompleter.forEach(AppConsoleReader.this::addCompleter);
            }
            while (true) {
                String res;
                try {
                    res = readLine(currentPrompt, mask, inputBuffer);
                } catch (UserInterruptException e) {
                    if (denyInterrupt) {
                        if (interruptNewPrompt != null) {
                            currentPrompt = interruptNewPrompt.apply(e);
                        }
                        continue;
                    }
                    if (exitCode != null) {
                        System.exit(exitCode);
                    }
                    throw e;
                }
                if (notEmpty) {
                    if (res.isEmpty()) {
                        if (newPrompt != null) {
                            currentPrompt = newPrompt.apply(res);
                        }
                        continue;
                    }
                }
                if (checker != null && !checker.test(res)) {
                    if (newPrompt != null) {
                        currentPrompt = newPrompt.apply(res);
                    }
                    continue;
                }
                if (tempCompleter != null) {
                    tempCompleter.forEach(AppConsoleReader.this::removeCompleter);
                    completerManager.undoActiveCompleter();
                }
                return res;
            }
        }

        /**
         * 添加临时使用的completer,
         * 此次读取完成后, 恢复回原来的completer
         */
        public ReadLineConfig addCompleter(Completer completer) {
            if (tempCompleter == null) {
                completerManager.markActiveCompleter();
                tempCompleter = new LinkedList<>();
            }
            tempCompleter.add(completer);
            return this;
        }

        public ReadLineConfig offActiveCompleter() {
            this.tempOffActiveCompleter = !tempOffActiveCompleter;
            return this;
        }

        public ReadLineConfig notEmpty() {
            this.notEmpty = !this.notEmpty;
            return this;
        }

        public ReadLineConfig denyInterrupt() {
            this.denyInterrupt = !this.notEmpty;
            return this;
        }

        public ReadLineConfig mask(Character mask) {
            this.mask = mask;
            return this;
        }

        public ReadLineConfig prompt(String prompt) {
            this.prompt = prompt;
            return this;
        }

        public ReadLineConfig newPrompt(Function<String, String> newPrompt) {
            this.newPrompt = newPrompt;
            return this;
        }

        public ReadLineConfig interruptNewPrompt(Function<UserInterruptException, String> interruptNewPrompt) {
            this.interruptNewPrompt = interruptNewPrompt;
            return this;
        }

        public ReadLineConfig addBuffer(String buffer) {
            this.buffer.clear();
            this.buffer.add(buffer);
            return this;
        }

        public ReadLineConfig input(String inputBuffer) {
            this.buffer.add(inputBuffer);
            return this;
        }

        public ReadLineConfig password() {
            this.mask = '*';
            return this;
        }

        public ReadLineConfig exitCode(Integer exitCode) {
            this.exitCode = exitCode;
            return this;
        }

        public ReadLineConfig exit() {
            this.exitCode = 0;
            return this;
        }

        public ReadLineConfig check(Predicate<String> checker) {
            this.checker = checker;
            return this;
        }

        @Override
        public String toString() {
            try {
                return apply();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
